/*
 * Copyright 2010 Alessandro Bellucci
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package org.soupframework.gwt.designerpanel.interaction.mouse.client;

import java.util.ArrayList;
import java.util.List;

import org.soupframework.gwt.designerpanel.client.designer.ContainerDesigner;
import org.soupframework.gwt.designerpanel.client.designer.Designer;
import org.soupframework.gwt.designerpanel.client.designer.OperatingMode;
import org.soupframework.gwt.designerpanel.client.interaction.InteractionHandler;
import org.soupframework.gwt.designerpanel.client.interaction.InteractionHandlerManager;
import org.soupframework.gwt.designerpanel.client.interaction.InteractionMode;
import org.soupframework.gwt.designerpanel.client.interaction.impl.BaseContainerInteractionHandler;
import org.soupframework.gwt.designerpanel.client.util.Util;

import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.HasClickHandlers;
import com.google.gwt.event.dom.client.HasMouseDownHandlers;
import com.google.gwt.event.dom.client.HasMouseUpHandlers;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseDownHandler;
import com.google.gwt.event.dom.client.MouseEvent;
import com.google.gwt.event.dom.client.MouseUpEvent;
import com.google.gwt.event.dom.client.MouseUpHandler;
import com.google.gwt.event.shared.GwtEvent;

public abstract class ContainerMouseInteractionHandler<T, CD extends ContainerDesigner<T>> extends BaseContainerInteractionHandler<T, CD> implements MouseDownHandler, MouseUpHandler, ClickHandler {

	protected final static int RADIUS = 3;

	protected static boolean ctrlKey = false;

	// the following mousedown state must be static because mouse down event is fired only once
	protected static boolean mouseDown = false;
	protected static int mouseDownX = -1;
	protected static int mouseDownY = -1;

	protected static int mouseX = -1;
	protected static int mouseY = -1;
	protected int lastHandledMouseX = -1;
	protected int lastHandledMouseY = -1;

	protected int mouseDownXRelative = -1;
	protected int mouseDownYRelative = -1;
	protected int mouseXRelative = -1;
	protected int mouseYRelative = -1;

	protected int mouseDownEventCount = 0;

	protected boolean marquee = false;
	protected int marqueeStartX = -1;
	protected int marqueeStartY = -1;
	protected int marqueeStartXRelative = -1;
	protected int marqueeStartYRelative = -1;

	protected List<T> objectsToRemoveFromSelection = new ArrayList<T>();

	public ContainerMouseInteractionHandler(InteractionHandlerManager<T> manager, CD designer){
		super(manager, designer);
	}

	@Override
	public void registerHandlersOnTarget(){
		if(!(this.getDesigner().getManager().getDesignerPanel() instanceof HasMouseDownHandlers)){
			throw new IllegalArgumentException("Target widget must implement HasMouseDownHandlers interface");
		}
		if(!(this.getDesigner().getManager().getDesignerPanel() instanceof HasMouseUpHandlers)){
			throw new IllegalArgumentException("Target widget must implement HasMouseUpHandlers interface");
		}
		if(!(this.getDesigner().getManager().getDesignerPanel() instanceof HasClickHandlers)){
			throw new IllegalArgumentException("Target widget must implement HasClickHandlers interface");
		}

		((HasMouseDownHandlers)this.getDesigner().getManager().getDesignerPanel()).addMouseDownHandler(this);
		((HasMouseUpHandlers)this.getDesigner().getManager().getDesignerPanel()).addMouseUpHandler(this);
		((HasClickHandlers)this.getDesigner().getManager().getDesignerPanel()).addClickHandler(this);
	}

	@Override
	public void onMouseDown(MouseDownEvent event) {
		this.dispatchEvent(event);
	}

	@Override
	public void onMouseUp(MouseUpEvent event) {
		this.dispatchEvent(event);
	}

	@Override
	public void onClick(ClickEvent event) {
		this.dispatchEvent(event);
	}

	protected boolean dispatchEvent(GwtEvent<?> event) {
		if(!(this.getDesigner().getManager().getOperatingMode() == OperatingMode.DESIGN_MODE || this.getDesigner().getManager().getOperatingMode() == OperatingMode.LIVE_DESIGN_MODE)){
			// events are only handled in DESIGN or LIVE DESIGN modes
			return true;
		}

		//System.out.println(event.getClass());
		if(event instanceof MouseEvent<?>){
			// Mouse position must be updated before calling dispatchEventToChildren
			mouseX = ((MouseEvent<?>)event).getClientX();
			mouseY = ((MouseEvent<?>)event).getClientY();
			this.mouseXRelative = ((MouseEvent<?>)event).getRelativeX(this.getDesigner().getObjectView().getElement());
			this.mouseYRelative = ((MouseEvent<?>)event).getRelativeY(this.getDesigner().getObjectView().getElement());
		}

		Designer<T> childDesigner = this.dispatchEventToChildren(event);
		boolean childRet = childDesigner != null;
		boolean localRet = this.dispatchEventInt(event, childDesigner);
		if(localRet == true){
			this.lastHandledMouseX = mouseX;
			this.lastHandledMouseY = mouseY;
		}
		return localRet || childRet;
	}

	protected boolean dispatchEventInt(GwtEvent<?> event, Designer<T> childDesigner) {
		if(event instanceof MouseDownEvent){
			this.mouseDownEventCount ++;
			return this.onMouseDownInt((MouseDownEvent)event, childDesigner);
		}
		else if(event instanceof MouseUpEvent){
			return this.onMouseUpInt((MouseUpEvent)event, childDesigner);
		}
		else if(event instanceof ClickEvent){
			return this.onClickInt((ClickEvent)event, childDesigner);
		}
		return false;
	}

	/**
	 * Dispatch an event to the first child designer that contains mouse coords
	 *
	 * @param event The event
	 * @return the {@link Designer} that received the event,null otherwise
	 */
	@SuppressWarnings("unchecked")
	protected Designer<T> dispatchEventToChildren(GwtEvent<?> event) {
		for(Designer<T> childDesigner : this.getDesigner().getChildrenDesigners()) {
			InteractionHandler<T> childInteractionHandler = this.getManager().getInteractionHandler(childDesigner);
			if(childInteractionHandler != null) {
				if(Util.widgetContainsCoords(childDesigner.getObjectView(), mouseX, mouseY)){
					if(childInteractionHandler instanceof ContainerMouseInteractionHandler<?, ?>){
						ContainerMouseInteractionHandler<T, ?> mouseInteractionHandler = ((ContainerMouseInteractionHandler<T, ?>)childInteractionHandler);
						if(mouseInteractionHandler.dispatchEvent(event)){
							return childDesigner;
						}
					}
				}
			}
		}
		return null;
	}

	/**
	 * Handles a {@link MouseDownEvent}
	 *
	 * @param event The {@link MouseDownEvent}
	 * @param childDesigner The child {@link Designer} that already handled this event
	 */
	protected boolean onMouseDownInt(MouseDownEvent event, Designer<T> childDesigner) {
		if(childDesigner != null){
			// child designers selection erases this designer selection
			if(this.getManager().getInteractionMode() == InteractionMode.SELECT || this.getManager().getInteractionMode() == InteractionMode.MOVE){
				if(childDesigner instanceof ContainerDesigner<?> && ((ContainerDesigner<T>)childDesigner).getSelectedObjectsCount(true) > 0){
					this.clearSelectionExcludeChildDesigner(childDesigner);
				}
			}

			// MouseDown events are only handled by foremost designers unless nothing has been selected in the child designer or its descendants
			if(childDesigner instanceof ContainerDesigner<?>){
				if(((ContainerDesigner<T>)childDesigner).getSelectedObjectsCount(true) > 0){
					return false;
				}
			}
		}

		if(event.getNativeButton() == NativeEvent.BUTTON_RIGHT){
			T object = this.getDesigner().getForemostObjectAtCoords(mouseX, mouseY);
			if(this.getDesigner().isObjectSelected(object)){
				this.getDesigner().openSelectionContextMenu(mouseX, mouseY);
			}
			else{
				this.getDesigner().openContextMenu(mouseX, mouseY);
			}
			event.stopPropagation();
			return true;
		}
		this.getDesignerManager().getDesignerPanel().hideContextMenu();

		if(event.getNativeButton() != NativeEvent.BUTTON_LEFT){
			return true;
		}

		// store values for click and up event handlers
		mouseDown = true;
		mouseDownX = mouseX;
		mouseDownY = mouseY;
		mouseDownXRelative = this.mouseXRelative;
		mouseDownYRelative = this.mouseYRelative;
		ctrlKey = event.isControlKeyDown();

		if(this.getManager().getInteractionMode() == InteractionMode.ADD_NEW){
			this.getDesigner().clearSelection();
			if(this.getManager().getNewObjectsHandler() != null){
				T newObject = this.getManager().getNewObjectsHandler().getNewObject();
				if(newObject != null){
					this.addObjectInt(newObject, this.mouseDownXRelative, this.mouseDownYRelative);
					// select the new widget and deselect the rest
					this.getDesigner().clearSelection(false);
					this.getDesigner().addObjectToSelection(newObject);
				}
			}
		}
		else if(this.getManager().getInteractionMode() == InteractionMode.SELECT){
			// select objects
			List<T> os = new ArrayList<T>();
			T fo = this.getDesigner().getForemostObjectAtCoords(mouseDownX, mouseDownY);
			if(fo != null){
				os.add(fo);
			}
			if(os.size() == 0 && this.getDesigner() == this.getManager().getRootDesigner()){
				this.marquee = true;
				this.marqueeStartX = mouseDownX;
				this.marqueeStartY = mouseDownY;
				this.marqueeStartXRelative = this.mouseDownXRelative;
				this.marqueeStartYRelative = this.mouseDownYRelative;
				this.getDesigner().clearSelection();
			}
			else{
				if(os.size() > 0){
					if(!ctrlKey){
						if(this.getDesigner().areObjectsSelected(os)){
							// widgets that were selected before click that are not being clicked will be deselected in the on mouse up event handler
							this.objectsToRemoveFromSelection.clear();
							for(T object : this.getDesigner().getSelectedObjects(false)){
								if(!os.contains(object)){
									this.objectsToRemoveFromSelection.add(object);
								}
							}

							// the event will be fired in on mouse up event handler
							this.getDesigner().addObjectsToSelection(os, false);
						}
						else{
							this.getDesigner().setSelection(os);
						}
					}
				}
				else{
					this.getDesigner().clearSelection();
				}
			}
		}
		return true;
	}

	/**
	 * Adds a new object to the {@link ContainerDesigner} as near as possible to the given coordinates
	 *
	 * @param newObject the new object to add
	 * @param relativeMouseX the coordinate x
	 * @param relativeMouseY the coordinate y
	 */
	protected abstract void addObjectInt(T newObject, int relativeMouseX, int relativeMouseY);

	/**
	 * Handles a {@link MouseUpEvent}
	 *
	 * @param event The {@link MouseUpEvent}
	 * @param childDesigner The child {@link Designer} that already handled this event
	 */
	protected boolean onMouseUpInt(MouseUpEvent event, Designer<T> childDesigner) {
		if(childDesigner != null){
			// MouseUp events are only handled by foremost designers unless nothing has been selected in the child designer or its descendants
			if(childDesigner instanceof ContainerDesigner<?>){
				if(((ContainerDesigner<T>)childDesigner).getSelectedObjectsCount(true) > 0){
					return false;
				}
			}
		}

		mouseX = event.getClientX();
		mouseY = event.getClientY();
		this.mouseXRelative = event.getRelativeX(this.getDesigner().getObjectView().getElement());
		this.mouseYRelative = event.getRelativeY(this.getDesigner().getObjectView().getElement());

		if(mouseDown){
			mouseDown = false;
		}

		if(marquee == true){
			this.getDesigner().getManager().getDesignerPanel().showMarquee(this.marqueeStartXRelative, this.marqueeStartYRelative, this.mouseXRelative, this.mouseYRelative);
			Iterable<T> os = this.getDesigner().getObjectsInArea(this.marqueeStartX, this.marqueeStartY, mouseX, mouseY);
			this.getDesigner().setSelection(os);
			this.getDesigner().getManager().getDesignerPanel().hideMarquee();
			this.marquee = false;
			this.marqueeStartX = -1;
			this.marqueeStartY = -1;
			this.marqueeStartXRelative = -1;
			this.marqueeStartYRelative = -1;
		}
		else{
			// if the mouse up event if happening "far" from the mouse down event avoid any processing
			if(Math.abs(event.getClientX() - mouseDownX) > RADIUS || Math.abs(event.getClientY() - mouseDownY) > RADIUS){
				return true;
			}

			// toggle widgets selection
			List<T> os = new ArrayList<T>();
			for(T object : this.getDesigner().getObjectsAtCoords(mouseDownX, mouseDownY)){
				os.add(object);
			}
			if(ctrlKey){	// if control key was pressed when mouse button went down
				this.getDesigner().toggleObjectsSelection(os);
			}
			else{
				if(this.getManager().getInteractionMode() == InteractionMode.SELECT) {
					this.getDesigner().removeObjectsFromSelection(this.objectsToRemoveFromSelection);
				}
				this.objectsToRemoveFromSelection.clear();
			}
		}

		this.getManager().setInteractionMode(InteractionMode.SELECT);
		return true;
	}

	/**
	 * Handles a {@link ClickEvent}
	 *
	 * @param event The {@link ClickEvent}
	 * @param childDesigner The child {@link Designer} that already handled this event
	 */
	protected boolean onClickInt(ClickEvent event, Designer<T> childDesigner) {
		if(childDesigner != null){
			// Click events are only handled by foremost designers
			return false;
		}
		return true;
	}

	private void clearSelectionExcludeChildDesigner(Designer<T> childDesigner){
		for(Designer<T> d : this.getDesigner().getChildrenDesigners()){
			if(d != childDesigner && d instanceof ContainerDesigner<?>){
				((ContainerDesigner<T>)d).clearSelection(false);
			}
		}
		this.getDesigner().clearSelection(false, false);
	}
}
